/*
XPTracker - Alternative Agile Tracking Tool
Copyright (C) 2006-2009  Stephen Starkey

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
*/
package org.xptracker.web;

import com.google.inject.Inject;
import net.sourceforge.stripes.action.*;
import net.sourceforge.stripes.ajax.JavaScriptBuilder;
import net.sourceforge.stripes.controller.LifecycleStage;
import net.sourceforge.stripes.security.action.Secure;
import org.apache.commons.lang.StringUtils;
import org.displaytag.decorator.TableDecorator;
import org.xptracker.domain.*;
import org.xptracker.domain.Queue;
import org.xptracker.persistence.PersistenceManager;
import org.xptracker.search.Searcher;
import org.xptracker.services.EmailService;
import org.xptracker.util.CollectionUtils;

import java.io.StringReader;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.*;

@UrlBinding("/Planning.action")
public class PlanningActionBean extends StandardActionBean {
  private static final String SRC = "story-source";
  private static final String DEST = "story-dest";

  @SuppressWarnings({"UnusedDeclaration"})
  @Inject
  private EmailService emailer;

  @SuppressWarnings({"UnusedDeclaration"})
  @Inject
  private QueueNotifier queueNotifier;

  @SuppressWarnings({"UnusedDeclaration"})
  @Inject
  private Searcher searcher;

  private List<Story> stories;
  private StorySource currentSource;
  private StorySource currentDestination;
  private List<StorySource> sources;
  private List<StorySource> destinations;
  private Map<String, Integer> sizeToPoints;

  public String getSizeToPoints() { return new JavaScriptBuilder(sizeToPoints).build(); }

  private Map<String, String> order;
  public Map<String, String> getOrder() { return order; }
  public void setOrder(Map<String, String> order) { this.order = order; }

  private Map<String, String> estimate;
  public Map<String, String> getEstimate() { return estimate; }
  public void setEstimate(Map<String, String> estimate) { this.estimate = estimate; }

  private Map<String, String> severity;
  public Map<String, String> getSeverity() {return severity; }
  public void setSeverity(Map<String, String> severity) {this.severity = severity;}

  private Map<String, String> size;
  public Map<String, String> getSize() { return size; }
  public void setSize(Map<String, String> size) { this.size = size; }

  private Map<String, String> move;
  public Map<String, String> getMove() { return move; }
  public void setMove(Map<String, String> move) { this.move = move; }

  private String src;
  public String getSrc() { return src; }
  public void setSrc(String src) { this.src = src; }

  private String destination;
  public String getDestination() { return destination; }
  public void setDestination(String destination) { this.destination = destination; }

  private String destinationRequest;
  public String getDestinationRequest() { return destinationRequest; }
  public void setDestinationRequest(String destinationRequest) { this.destinationRequest = destinationRequest; }

  @DefaultHandler
  public Resolution showPlans() {
    return new ForwardResolution("/planning.jsp");
  }

  @HandlesEvent("ApplyPlan")
  @Secure(roles="planner")
  public Resolution applyPlan() {
    return persistenceManager.inTransaction(new PersistenceManager.UpdateHandler<Resolution>() {
      public Resolution doUpdate(PersistenceManager.UpdateReceiver receiver) {
        SortedMap<Integer, Story> reorderings = new TreeMap<Integer, Story>();
        for(Story story: new ArrayList<Story>(currentSource.getStories(domain))) {
          double newEstimate = getEstimate(story);
          Size newSize = getSize(story);
          Severity newSeverity = getSeverity(story);

          boolean move = shouldMove(story);
          String order = getOrder().get(story.getId());

          //noinspection deprecation
          String oldOrder = param("old_order_" + story.getId());
          boolean reorder = !move && (!StringUtils.equals(order, oldOrder));

          StringBuffer blogEntry = new StringBuffer("*Planning Changes*\n");
          boolean modified = false;
          if (newEstimate != story.getEstimate()) {
            blogEntry.append("\n * Estimate changed from **").append(story.getEstimate()).append("** to **").append(newEstimate).append("**");
            getContext().getMessages().add(new SimpleMessage("Estimate changed for *{0}*.", story.toString()));
            story.setEstimate(newEstimate);
            modified = true;
          }

          if (newSize != story.getSize()) {
            blogEntry.append("\n * Size changed from **").append(story.getSize()).append("** to **").append(newSize).append("**");
            getContext().getMessages().add(new SimpleMessage("Size changed for *{0}*.", story.toString()));
            story.setSize(newSize);
            modified = true;
          }

          if (newSeverity != story.getSeverity()) {
            blogEntry.append("\n * Severity changed from **").append(story.getSeverity()).append("** to **").append(newSeverity).append("**");
            getContext().getMessages().add(new SimpleMessage("Severity changed for *{0}*.", story.toString()));
            story.setSeverity(newSeverity);
            modified = true;
          }

          if (move) {
            blogEntry.append("\n * Moved from **").append(currentSource.getName()).append("** to **").append(currentDestination.getName()).append("**");
            getContext().getMessages().add(new SimpleMessage("Moved story *{0}* to *{1}*.", story.toString(), currentDestination.getName()));
            currentDestination.addStory(domain, receiver, queueNotifier, story);
            modified = true;
          }

          if (reorder) {
            Integer orderInt = new Integer(order);
            if (!reorderings.containsKey(orderInt)) {
              getContext().getMessages().add(new SimpleMessage("Reordered story *{0}* from position *{1}* to position *{2}*", story.toString(), oldOrder, order));
              blogEntry.append("\n * Reordered from position **").append(oldOrder).append("** to position **").append(order).append("**");
              reorderings.put(orderInt, story);
              modified = true;
            }
          }

          if (modified) {
            StoryActionBean.addBlogEntry(getCurrentUser(), story, blogEntry.toString().trim(), PlanningActionBean.this,
                receiver, emailer);
          }
        }

        if (!reorderings.isEmpty()) {
          currentSource.changeOrder(domain, receiver, reorderings);
        }

        queueNotifier.sendNotifications(getCurrentUser());
        return new RedirectResolution(PlanningActionBean.class);
      }
    });
  }

  @HandlesEvent("GetEstimate")
  public Resolution retrieveDestinationEstimate() {
    StorySource destination = getDestinationSource();
    long pointsTotal = 0;
    double hoursTotal = 0.0;

    if (destination != null) {
      for (Story story : destination.getStories(domain)) {
        pointsTotal += domain.getPoints(story);
        hoursTotal += story.getEstimate();
      }

      for (Story story : currentSource.getStories(domain)) {
        if (shouldMove(story)) {
          pointsTotal += getPoints(domain, story);
          hoursTotal += getEstimate(story);
        }
      }
    }
    NumberFormat fmt = new DecimalFormat();
    fmt.setMinimumFractionDigits(0);
    fmt.setMaximumFractionDigits(2);
    fmt.setMinimumIntegerDigits(1);
    return new StreamingResolution("text", new StringReader(destinationRequest + ";" + fmt.format(hoursTotal) + ";" +
        pointsTotal));
  }

  private int getPoints(Domain domain, Story story) {
    Integer result = domain.loadPointMapping(true).getMap().get(getSize(story).toString());
    return result == null ? 0 : result;
  }

  private Size getSize(Story story) {
    return Size.valueOf(size.get(story.getId()));
  }

  private Severity getSeverity(Story story) {
    return Severity.valueOf(severity.get(story.getId()));
  }

  private Boolean shouldMove(Story story) {
    return Boolean.valueOf(move.get(story.getId()));
  }

  private double getEstimate(Story story) {
    return Double.parseDouble(zeroIfBlank(estimate.get(story.getId())));
  }

  private String zeroIfBlank(String s) {
    return StringUtils.isBlank(s) ? "0" : s;
  }

  @Before(stages=LifecycleStage.BindingAndValidation)
  public void setupCollections() {
    order = new HashMap<String, String>();
    estimate = new HashMap<String, String>();
    move = new HashMap<String, String>();
    sizeToPoints = domain.loadPointMapping(true).getMap();
  }

  @Before(stages=LifecycleStage.EventHandling)
  public void provideSourceItems() {
    sources = new LinkedList<StorySource>();
    destinations = new LinkedList<StorySource>();

    currentSource = getStorySource();
    currentDestination = getDestinationSource();
    stories = DomainUtil.fullyLoadItems(currentSource.getStories(domain));

    Set<State> storyStates = new HashSet<State>();
    for(Story story: stories) {
      storyStates.add(story.getState());
    }

    sources.add(new StateSource(State.dreaming));

    if (storyStates.size() == 1) {
      for(State state: org.xptracker.services.StoryState.getNextActionStates(storyStates.iterator().next())) {
        if (org.xptracker.services.StoryState.getInfo(state).massMoveSupported()) {
          StateSource dSrc = new StateSource(state);
          if (!dSrc.equals(currentSource)) {
            destinations.add(dSrc);
          }
        }
      }
    }
    
    destinations.add(new StateSource(State.rejected));

    for(SavedQuery query: CollectionUtils.sort(getCurrentUser().getQueries(), "name")) {
      sources.add(new SavedQuerySource(query, searcher));
    }

    for(Queue queue : domain.listQueues(false)) {
      QueueSource qSrc = new QueueSource(queue);
      sources.add(qSrc);
      if (!qSrc.equals(currentSource)) {
        destinations.add(qSrc);
      }
    }
  }

  @After(stages=LifecycleStage.HandlerResolution)
  public void provideTableDecorator() {
    getContext().getRequest().setAttribute("planningDecorator", new TableDecorator() {
      public Story getStory() {
        return (Story) getCurrentRowObject();
      }

      public String addRowClass() {
        return getStory().getSeverity().toString();
      }

      public String addRowId() {
        return "row_" + getStory().getId();
      }
    });
  }

  private StorySource getStorySource() {
    return loadAndRemember(src, SRC);
  }

  private StorySource getDestinationSource() {
    return loadAndRemember(destination, DEST);
  }

  private StorySource loadAndRemember(String srcId, String attr) {
    if (StringUtils.isNotEmpty(srcId)) {
      StorySource source = getSource(srcId);
      if (source != null) {
        getContext().getRequest().getSession().setAttribute(attr, source);
        return source;
      }
    }

    StorySource source = (StorySource) getContext().getRequest().getSession().getAttribute(attr);
    if (source == null || !source.isAlive(domain)) { //todo Attempt to reload id-based source if possible?  Only important for clustered impl.
      source = new StateSource(State.dreaming);
      getContext().getRequest().getSession().removeAttribute(attr);
    }
    return source;
  }

  private StorySource getSource(String srcId) {
    StorySource source = null;
    if (StateSource.isStateSource(srcId)) {
      source = new StateSource(srcId);
    } else if (SavedQuerySource.isSavedQuerySource(srcId)) {
      source = SavedQuerySource.newInstance(srcId, domain, searcher);
    } else {
      Queue queue = loadEntity(Queue.class, srcId);
      if (queue != null) {
        source = new QueueSource(queue);
      }
    }
    return source;
  }

  public List<Story> getStories() {
    return stories;
  }

  public void setStories(List<Story> stories) {
    this.stories = stories;
  }

  public StorySource getCurrentSource() {
    return currentSource;
  }

  public void setCurrentSource(StorySource currentSource) {
    this.currentSource = currentSource;
  }

  public StorySource getCurrentDestination() {
    return currentDestination;
  }

  public void setCurrentDestination(StorySource currentDestination) {
    this.currentDestination = currentDestination;
  }

  public List<StorySource> getSources() {
    return sources;
  }

  public void setSources(List<StorySource> sources) {
    this.sources = sources;
  }

  public List<StorySource> getDestinations() {
    return destinations;
  }

  public void setDestinations(List<StorySource> destinations) {
    this.destinations = destinations;
  }
}
